/*
 *    Copyright (c) 2024 Project CHIP Authors
 *    All rights reserved.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
#include <data-model-providers/codegen/CodegenDataModelProvider.h>

#include <access/AccessControl.h>
#include <access/Privilege.h>
#include <app-common/zap-generated/attribute-type.h>
#include <app/CommandHandlerInterface.h>
#include <app/CommandHandlerInterfaceRegistry.h>
#include <app/ConcreteAttributePath.h>
#include <app/ConcreteClusterPath.h>
#include <app/ConcreteCommandPath.h>
#include <app/EventPathParams.h>
#include <app/GlobalAttributes.h>
#include <app/RequiredPrivilege.h>
#include <app/data-model-provider/MetadataTypes.h>
#include <app/data-model-provider/Provider.h>
#include <app/persistence/AttributePersistenceProvider.h>
#include <app/persistence/AttributePersistenceProviderInstance.h>
#include <app/persistence/DefaultAttributePersistenceProvider.h>
#include <app/server-cluster/ServerClusterContext.h>
#include <app/server-cluster/ServerClusterInterface.h>
#include <app/util/DataModelHandler.h>
#include <app/util/IMClusterCommandHandler.h>
#include <app/util/af-types.h>
#include <app/util/attribute-metadata.h>
#include <app/util/attribute-storage.h>
#include <app/util/endpoint-config-api.h>
#include <lib/core/CHIPError.h>
#include <lib/core/DataModelTypes.h>
#include <lib/support/CodeUtils.h>
#include <lib/support/ReadOnlyBuffer.h>
#include <lib/support/ScopedBuffer.h>

#include <cstdint>
#include <optional>

namespace chip {
namespace app {
namespace {

DataModel::AcceptedCommandEntry AcceptedCommandEntryFor(const ConcreteCommandPath & path)
{
    const CommandId commandId = path.mCommandId;

    DataModel::AcceptedCommandEntry entry(
        path.mCommandId,
        BitFlags<DataModel::CommandQualityFlags>{}
            .Set(DataModel::CommandQualityFlags::kTimed, CommandNeedsTimedInvoke(path.mClusterId, commandId))
            .Set(DataModel::CommandQualityFlags::kFabricScoped, CommandIsFabricScoped(path.mClusterId, commandId))
            .Set(DataModel::CommandQualityFlags::kLargeMessage, CommandHasLargePayload(path.mClusterId, commandId)),
        RequiredPrivilege::ForInvokeCommand(path));

    return entry;
}

DataModel::ServerClusterEntry ServerClusterEntryFrom(EndpointId endpointId, const EmberAfCluster & cluster)
{
    DataModel::ServerClusterEntry entry;

    entry.clusterId = cluster.clusterId;

    DataVersion * versionPtr = emberAfDataVersionStorage(ConcreteClusterPath(endpointId, cluster.clusterId));
    if (versionPtr == nullptr)
    {
#if CHIP_CONFIG_DATA_MODEL_EXTRA_LOGGING
        ChipLogError(AppServer, "Failed to get data version for %d/" ChipLogFormatMEI, endpointId,
                     ChipLogValueMEI(cluster.clusterId));
#endif
        entry.dataVersion = 0;
    }
    else
    {
        entry.dataVersion = *versionPtr;
    }

    // TODO: set entry flags:
    //   entry.flags.Set(ClusterQualityFlags::kDiagnosticsData)

    return entry;
}

DataModel::AttributeEntry AttributeEntryFrom(const ConcreteClusterPath & clusterPath, const EmberAfAttributeMetadata & attribute)
{
    const ConcreteAttributePath attributePath(clusterPath.mEndpointId, clusterPath.mClusterId, attribute.attributeId);

    using DataModel::AttributeQualityFlags;

    DataModel::AttributeEntry entry(
        attribute.attributeId,
        BitFlags<DataModel::AttributeQualityFlags>{}
            .Set(AttributeQualityFlags::kListAttribute, (attribute.attributeType == ZCL_ARRAY_ATTRIBUTE_TYPE))
            .Set(DataModel::AttributeQualityFlags::kTimed, attribute.MustUseTimedWrite()),
        attribute.IsReadable() ? std::make_optional(RequiredPrivilege::ForReadAttribute(attributePath)) : std::nullopt,
        attribute.IsWritable() ? std::make_optional(RequiredPrivilege::ForWriteAttribute(attributePath)) : std::nullopt);

    // NOTE: we do NOT provide additional info for:
    //    - IsExternal/IsAutomaticallyPersisted is not used by IM handling
    //    - Several specification flags are not available (reportable, quieter reporting,
    //      fixed, source attribution)

    // TODO: Set additional flags:
    // entry.flags.Set(DataModel::AttributeQualityFlags::kFabricScoped)
    // entry.flags.Set(DataModel::AttributeQualityFlags::kFabricSensitive)
    // entry.flags.Set(DataModel::AttributeQualityFlags::kChangesOmitted)
    return entry;
}

DefaultAttributePersistenceProvider gDefaultAttributePersistence;

} // namespace

CHIP_ERROR CodegenDataModelProvider::Shutdown()
{
    Reset();
    mContext.reset();
    mRegistry.ClearContext();
    return DataModel::Provider::Shutdown();
}

CHIP_ERROR CodegenDataModelProvider::Startup(DataModel::InteractionModelContext context)
{
    // server clusters require a valid persistent storage delegate
    VerifyOrReturnError(mPersistentStorageDelegate != nullptr, CHIP_ERROR_INCORRECT_STATE);
    ReturnErrorOnFailure(DataModel::Provider::Startup(context));

    mContext.emplace(context);

    // Ember NVM requires have a data model provider. attempt to create one if one is not available
    //
    // It is not a critical failure to not have one, however if one is not set up, ember NVM operations
    // will error out with a `persistence not available`.
    if (GetAttributePersistenceProvider() == nullptr)
    {
#if CHIP_CONFIG_DATA_MODEL_EXTRA_LOGGING
        ChipLogProgress(DataManagement, "Ember attribute persistence requires setting up");
#endif
        ReturnErrorOnFailure(gDefaultAttributePersistence.Init(mPersistentStorageDelegate));
        SetAttributePersistenceProvider(&gDefaultAttributePersistence);
    }

    InitDataModelForTesting();

    return mRegistry.SetContext(ServerClusterContext{
        .provider           = *this,
        .storage            = *mPersistentStorageDelegate,
        .attributeStorage   = *GetAttributePersistenceProvider(), // guaranteed set up by the above logic
        .interactionContext = *mContext,                          // NOLINT(bugprone-unchecked-optional-access): emplaced above
    });
}

std::optional<DataModel::ActionReturnStatus> CodegenDataModelProvider::InvokeCommand(const DataModel::InvokeRequest & request,
                                                                                     TLV::TLVReader & input_arguments,
                                                                                     CommandHandler * handler)
{
    if (auto * cluster = mRegistry.Get(request.path); cluster != nullptr)
    {
        return cluster->InvokeCommand(request, input_arguments, handler);
    }

    CommandHandlerInterface * handler_interface =
        CommandHandlerInterfaceRegistry::Instance().GetCommandHandler(request.path.mEndpointId, request.path.mClusterId);

    if (handler_interface)
    {
        CommandHandlerInterface::HandlerContext context(*handler, request.path, input_arguments);
        handler_interface->InvokeCommand(context);

        // If the command was handled, don't proceed any further and return successfully.
        if (context.mCommandHandled)
        {
            return std::nullopt;
        }
    }

    // Ember always returns responses via the handler, so std::nullopt must be returned here to follow the InvokeCommand API
    // contract
    DispatchSingleClusterCommand(request.path, input_arguments, handler);
    return std::nullopt;
}

CHIP_ERROR CodegenDataModelProvider::Endpoints(ReadOnlyBufferBuilder<DataModel::EndpointEntry> & builder)
{
    const uint16_t endpointCount = emberAfEndpointCount();

    ReturnErrorOnFailure(builder.EnsureAppendCapacity(endpointCount));

    for (uint16_t endpointIndex = 0; endpointIndex < endpointCount; endpointIndex++)
    {
        if (!emberAfEndpointIndexIsEnabled(endpointIndex))
        {
            continue;
        }

        DataModel::EndpointEntry entry;
        entry.id       = emberAfEndpointFromIndex(endpointIndex);
        entry.parentId = emberAfParentEndpointFromIndex(endpointIndex);

        switch (GetCompositionForEndpointIndex(endpointIndex))
        {
        case EndpointComposition::kFullFamily:
            entry.compositionPattern = DataModel::EndpointCompositionPattern::kFullFamily;
            break;
        case EndpointComposition::kTree:
        case EndpointComposition::kInvalid: // should NOT happen, but force compiler to check we validate all versions
            entry.compositionPattern = DataModel::EndpointCompositionPattern::kTree;
            break;
        }
        ReturnErrorOnFailure(builder.Append(entry));
    }

    return CHIP_NO_ERROR;
}

std::optional<unsigned> CodegenDataModelProvider::TryFindEndpointIndex(EndpointId id) const
{
    const uint16_t lastEndpointIndex = emberAfEndpointCount();

    if ((mEndpointIterationHint < lastEndpointIndex) && emberAfEndpointIndexIsEnabled(mEndpointIterationHint) &&
        (id == emberAfEndpointFromIndex(mEndpointIterationHint)))
    {
        return std::make_optional(mEndpointIterationHint);
    }

    // Linear search, this may be slow
    uint16_t idx = emberAfIndexFromEndpoint(id);
    if (idx == kEmberInvalidEndpointIndex)
    {
        return std::nullopt;
    }

    return std::make_optional<unsigned>(idx);
}

CHIP_ERROR CodegenDataModelProvider::EventInfo(const ConcreteEventPath & path, DataModel::EventEntry & eventInfo)
{
    if (auto * cluster = mRegistry.Get(path); cluster != nullptr)
    {
        return cluster->EventInfo(path, eventInfo);
    }

    eventInfo.readPrivilege = RequiredPrivilege::ForReadEvent(path);
    return CHIP_NO_ERROR;
}

CHIP_ERROR CodegenDataModelProvider::ServerClusters(EndpointId endpointId,
                                                    ReadOnlyBufferBuilder<DataModel::ServerClusterEntry> & builder)
{
    const EmberAfEndpointType * endpoint = emberAfFindEndpointType(endpointId);

    VerifyOrReturnValue(endpoint != nullptr, CHIP_ERROR_NOT_FOUND);
    VerifyOrReturnValue(endpoint->clusterCount > 0, CHIP_NO_ERROR);
    VerifyOrReturnValue(endpoint->cluster != nullptr, CHIP_NO_ERROR);

    // We build the cluster list by merging two lists:
    //   - mRegistry items from ServerClusterInterfaces
    //   - ember metadata clusters
    //
    // This is done because `ServerClusterInterface` allows full control for all its metadata,
    // in particular `data version` and `flags`.
    //
    // To allow cluster implementations to be incrementally converted to storing their own data versions,
    // instead of relying on the out-of-band emberAfDataVersionStorage, first check for clusters that are
    // using the new data version storage and are registered via SingleEndpointServerClusterRegistry, then fill
    // in the data versions for the rest via the out-of-band mechanism.

    // assume the clusters on endpoint does not change in between these two loops
    auto clusters               = mRegistry.ClustersOnEndpoint(endpointId);
    size_t registryClusterCount = 0;
    for ([[maybe_unused]] auto _ : clusters)
    {
        registryClusterCount++;
    }

    ReturnErrorOnFailure(builder.EnsureAppendCapacity(registryClusterCount));

    ReadOnlyBufferBuilder<ClusterId> knownClustersBuilder;
    ReturnErrorOnFailure(knownClustersBuilder.EnsureAppendCapacity(registryClusterCount));
    for (const auto clusterId : mRegistry.ClustersOnEndpoint(endpointId))
    {
        ConcreteClusterPath path(endpointId, clusterId);
        ServerClusterInterface * cluster = mRegistry.Get(path);

        // path MUST be valid: we just got it from iterating our registrations...
        VerifyOrReturnError(cluster != nullptr, CHIP_ERROR_INTERNAL);

        ReturnErrorOnFailure(builder.Append({
            .clusterId   = path.mClusterId,
            .dataVersion = cluster->GetDataVersion(path),
            .flags       = cluster->GetClusterFlags(path),
        }));
        ReturnErrorOnFailure(knownClustersBuilder.Append(path.mClusterId));
    }

    ReadOnlyBuffer<ClusterId> knownClusters = knownClustersBuilder.TakeBuffer();

    ReturnErrorOnFailure(builder.EnsureAppendCapacity(emberAfClusterCountForEndpointType(endpoint, /* server = */ true)));

    const EmberAfCluster * begin = endpoint->cluster;
    const EmberAfCluster * end   = endpoint->cluster + endpoint->clusterCount;
    for (const EmberAfCluster * cluster = begin; cluster != end; cluster++)
    {
        if (!cluster->IsServer())
        {
            continue;
        }

        // linear search as this is a somewhat compact number list, so performance is probably not too bad
        // This results in smaller code than some memory allocation + std::sort + std::binary_search
        bool found = false;
        for (ClusterId clusterId : knownClusters)
        {
            if (clusterId == cluster->clusterId)
            {
                found = true;
                break;
            }
        }
        if (found)
        {
            // value already filled from the ServerClusterRegistry. That one has the correct/overriden
            // flags and data version
            continue;
        }

        ReturnErrorOnFailure(builder.Append(ServerClusterEntryFrom(endpointId, *cluster)));
    }

    return CHIP_NO_ERROR;
}

CHIP_ERROR CodegenDataModelProvider::Attributes(const ConcreteClusterPath & path,
                                                ReadOnlyBufferBuilder<DataModel::AttributeEntry> & builder)
{
    if (auto * cluster = mRegistry.Get(path); cluster != nullptr)
    {
        return cluster->Attributes(path, builder);
    }

    const EmberAfCluster * cluster = FindServerCluster(path);

    VerifyOrReturnValue(cluster != nullptr, CHIP_ERROR_NOT_FOUND);
    VerifyOrReturnValue(cluster->attributeCount > 0, CHIP_NO_ERROR);
    VerifyOrReturnValue(cluster->attributes != nullptr, CHIP_NO_ERROR);

    // TODO: if ember would encode data in AttributeEntry form, we could reference things directly (shorter code,
    //       although still allocation overhead due to global attributes not in metadata)
    //
    // We have Attributes from ember + global attributes that are NOT in ember metadata.
    // We have to report them all
    constexpr size_t kGlobalAttributeNotInMetadataCount = MATTER_ARRAY_SIZE(GlobalAttributesNotInMetadata);

    ReturnErrorOnFailure(builder.EnsureAppendCapacity(cluster->attributeCount + kGlobalAttributeNotInMetadataCount));

    Span<const EmberAfAttributeMetadata> attributeSpan(cluster->attributes, cluster->attributeCount);

    for (auto & attribute : attributeSpan)
    {
        ReturnErrorOnFailure(builder.Append(AttributeEntryFrom(path, attribute)));
    }

    for (auto & attributeId : GlobalAttributesNotInMetadata)
    {

        // This "GlobalListEntry" is specific for metadata that ember does not include
        // in its attribute list metadata.
        //
        // By spec these Attribute/AcceptedCommands/GeneratedCommants lists are:
        //   - lists of elements
        //   - read-only, with read privilege view
        //   - fixed value (no such flag exists, so this is not a quality flag we set/track)
        DataModel::AttributeEntry globalListEntry(attributeId, DataModel::AttributeQualityFlags::kListAttribute,
                                                  Access::Privilege::kView, std::nullopt);

        ReturnErrorOnFailure(builder.Append(std::move(globalListEntry)));
    }

    return CHIP_NO_ERROR;
}

CHIP_ERROR CodegenDataModelProvider::ClientClusters(EndpointId endpointId, ReadOnlyBufferBuilder<ClusterId> & builder)
{
    const EmberAfEndpointType * endpoint = emberAfFindEndpointType(endpointId);

    VerifyOrReturnValue(endpoint != nullptr, CHIP_ERROR_NOT_FOUND);
    VerifyOrReturnValue(endpoint->clusterCount > 0, CHIP_NO_ERROR);
    VerifyOrReturnValue(endpoint->cluster != nullptr, CHIP_NO_ERROR);

    ReturnErrorOnFailure(builder.EnsureAppendCapacity(emberAfClusterCountForEndpointType(endpoint, /* server = */ false)));

    const EmberAfCluster * begin = endpoint->cluster;
    const EmberAfCluster * end   = endpoint->cluster + endpoint->clusterCount;
    for (const EmberAfCluster * cluster = begin; cluster != end; cluster++)
    {
        if (!cluster->IsClient())
        {
            continue;
        }
        ReturnErrorOnFailure(builder.Append(cluster->clusterId));
    }

    return CHIP_NO_ERROR;
}

const EmberAfCluster * CodegenDataModelProvider::FindServerCluster(const ConcreteClusterPath & path)
{
    if (mPreviouslyFoundCluster.has_value() && (mPreviouslyFoundCluster->path == path) &&
        (mEmberMetadataStructureGeneration == emberAfMetadataStructureGeneration()))

    {
        return mPreviouslyFoundCluster->cluster;
    }

    const EmberAfCluster * cluster = emberAfFindServerCluster(path.mEndpointId, path.mClusterId);
    if (cluster != nullptr)
    {
        mPreviouslyFoundCluster           = std::make_optional<ClusterReference>(path, cluster);
        mEmberMetadataStructureGeneration = emberAfMetadataStructureGeneration();
    }
    return cluster;
}

CHIP_ERROR CodegenDataModelProvider::AcceptedCommands(const ConcreteClusterPath & path,
                                                      ReadOnlyBufferBuilder<DataModel::AcceptedCommandEntry> & builder)
{
    if (auto * cluster = mRegistry.Get(path); cluster != nullptr)
    {
        return cluster->AcceptedCommands(path, builder);
    }

    // Some CommandHandlerInterface instances are registered of ALL endpoints, so make sure first that
    // the cluster actually exists on this endpoint before asking the CommandHandlerInterface what commands
    // it claims to support.
    const EmberAfCluster * serverCluster = FindServerCluster(path);
    VerifyOrReturnError(serverCluster != nullptr, CHIP_ERROR_NOT_FOUND);

    CommandHandlerInterface * interface =
        CommandHandlerInterfaceRegistry::Instance().GetCommandHandler(path.mEndpointId, path.mClusterId);
    if (interface != nullptr)
    {
        CHIP_ERROR err = interface->RetrieveAcceptedCommands(path, builder);
        // If retrieving the accepted commands returns CHIP_ERROR_NOT_IMPLEMENTED then continue with normal processing.
        // Otherwise we finished.
        VerifyOrReturnError(err == CHIP_ERROR_NOT_IMPLEMENTED, err);
    }
    VerifyOrReturnError(serverCluster->acceptedCommandList != nullptr, CHIP_NO_ERROR);

    const chip::CommandId * endOfList = serverCluster->acceptedCommandList;
    while (*endOfList != kInvalidCommandId)
    {
        endOfList++;
    }
    const auto commandCount = static_cast<size_t>(endOfList - serverCluster->acceptedCommandList);

    // TODO: if ember would store command entries, we could simplify this code to use static data
    ReturnErrorOnFailure(builder.EnsureAppendCapacity(commandCount));

    ConcreteCommandPath commandPath = ConcreteCommandPath(path.mEndpointId, path.mClusterId, kInvalidCommandId);
    for (const chip::CommandId * p = serverCluster->acceptedCommandList; p != endOfList; p++)
    {
        commandPath.mCommandId = *p;
        ReturnErrorOnFailure(builder.Append(AcceptedCommandEntryFor(commandPath)));
    }

    return CHIP_NO_ERROR;
}

CHIP_ERROR CodegenDataModelProvider::GeneratedCommands(const ConcreteClusterPath & path, ReadOnlyBufferBuilder<CommandId> & builder)
{
    if (auto * cluster = mRegistry.Get(path); cluster != nullptr)
    {
        return cluster->GeneratedCommands(path, builder);
    }

    // Some CommandHandlerInterface instances are registered of ALL endpoints, so make sure first that
    // the cluster actually exists on this endpoint before asking the CommandHandlerInterface what commands
    // it claims to support.
    const EmberAfCluster * serverCluster = FindServerCluster(path);
    VerifyOrReturnError(serverCluster != nullptr, CHIP_ERROR_NOT_FOUND);

    CommandHandlerInterface * interface =
        CommandHandlerInterfaceRegistry::Instance().GetCommandHandler(path.mEndpointId, path.mClusterId);
    if (interface != nullptr)
    {
        CHIP_ERROR err = interface->RetrieveGeneratedCommands(path, builder);
        // If retrieving generated commands returns CHIP_ERROR_NOT_IMPLEMENTED then continue with normal procesing.
        // Otherwise we finished.
        VerifyOrReturnError(err == CHIP_ERROR_NOT_IMPLEMENTED, err);
    }

    VerifyOrReturnError(serverCluster->generatedCommandList != nullptr, CHIP_NO_ERROR);

    const chip::CommandId * endOfList = serverCluster->generatedCommandList;
    while (*endOfList != kInvalidCommandId)
    {
        endOfList++;
    }
    const auto commandCount = static_cast<size_t>(endOfList - serverCluster->generatedCommandList);
    return builder.ReferenceExisting({ serverCluster->generatedCommandList, commandCount });
}

void CodegenDataModelProvider::InitDataModelForTesting()
{
    // Call the Ember-specific InitDataModelHandler
    InitDataModelHandler();
}

CHIP_ERROR CodegenDataModelProvider::DeviceTypes(EndpointId endpointId, ReadOnlyBufferBuilder<DataModel::DeviceTypeEntry> & builder)
{
    std::optional<unsigned> endpoint_index = TryFindEndpointIndex(endpointId);
    if (!endpoint_index.has_value())
    {
        return {};
    }

    CHIP_ERROR err = CHIP_NO_ERROR;

    return builder.ReferenceExisting(emberAfDeviceTypeListFromEndpointIndex(*endpoint_index, err));
}

#if CHIP_CONFIG_USE_ENDPOINT_UNIQUE_ID
CHIP_ERROR CodegenDataModelProvider::EndpointUniqueID(EndpointId endpointId, MutableCharSpan & epUniqueId)
{
    char buffer[Clusters::Descriptor::Attributes::EndpointUniqueID::TypeInfo::MaxLength()] = { 0 };
    MutableCharSpan epUniqueIdSpan(buffer);
    ReturnErrorOnFailure(emberAfGetEndpointUniqueIdForEndPoint(endpointId, epUniqueIdSpan));

    memcpy(epUniqueId.data(), epUniqueIdSpan.data(), epUniqueIdSpan.size());
    return CHIP_NO_ERROR;
}
#endif

} // namespace app
} // namespace chip
